package com.vincent.redispractise.dbClass.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.redispractise.dbClass.entity.SeckillVoucher;
import com.vincent.redispractise.dbClass.entity.VoucherOrder;
import com.vincent.redispractise.dbClass.mapper.SeckillVoucherMapper;
import com.vincent.redispractise.dbClass.mapper.VoucherOrderMapper;
import com.vincent.redispractise.part02.Idworker.IdWorker;
import com.vincent.redispractise.utils.CacheService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.concurrent.*;

@Slf4j
@Service
public class SeckillVoucherServiceImpl extends ServiceImpl<SeckillVoucherMapper, SeckillVoucher> implements SeckillVoucherService {

    @Autowired
    private CacheService cacheService;
    @Autowired
    private IdWorker idWorker;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private SeckillVoucherMapper seckillVoucherMapper;
    @Resource
    private VoucherOrderMapper voucherOrderMapper;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static  final DefaultRedisScript<Long> UNLOCK_SCRIPT ;
    static{
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    /*
     * 超卖场景
     */
    @Override
    @Transactional
    public String seckill1(Long id) {
        //1、查询秒杀优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherMapper.selectById(id);
        if (seckillVoucher == null) {
            return "param error";
        }
        //2、判断优惠券活动时间
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();
        if (LocalDateTime.now().isBefore(beginTime) || LocalDateTime.now().isAfter(endTime)) {
            return "seckill expire!";
        }
        //3、判断优惠券库存信息
        Integer stock = seckillVoucher.getStock();
        if (stock <= 0) {
            return "No inventory!";
        }
        //4、扣减库存
        update().setSql("stock = stock -1").eq("voucher_id", id).update();
        //5、下单
        VoucherOrder voucherOrder = new VoucherOrder().setId(idWorker.nextId("seckill:voucher")).setVoucherId(id).setUserId(1L);
        voucherOrderMapper.insert(voucherOrder);
        return "seckill sucess!";
    }

    /* 解决超卖场景：【乐观锁：版本号法->CAS法】
     * 实现原理：需要更新的数据库记录都带有一个版本号，从查询记录后那个该记录的版本号，然后更新时对比版本号，这样就可以确保数据从本次查询到更新没有修改过
     * 缺点：大量的并发请求，会导致大量的更新操作失败，导致少买的情况；
     * 【改进1】：减库存场景中，我们关注点是库存数不能变，所以将库存值作为版本号，只要判断库存数和查到的数一致，就认为这条记录在这段时间内没有数据更新
     * 缺点：依然存在大量的并发请求，会导致大量的更新操作失败，导致少买的情况；
     * 【改进2】：减库存场景中，我们关注点是库存数不能为0，所以改进版本号法为CAS,只判断库存数，如果大于0就更新（不是所有的版本号法能改成CAS法）
     */
    @Override
    @Transactional
    public String seckill2(Long id) {
        //1、查询秒杀优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherMapper.selectById(id);
        if (seckillVoucher == null) {
            return "param error";
        }
        //2、判断优惠券活动时间
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();
        if (LocalDateTime.now().isBefore(beginTime) || LocalDateTime.now().isAfter(endTime)) {
            return "seckill expire!";
        }
        //3、判断优惠券库存信息
        Integer stock = seckillVoucher.getStock();
        if (stock <= 0) {
            return "No inventory!";
        }
        //4、扣减库存
        //版本号法
        //boolean success = update().setSql("stock = stock -1").eq("voucher_id", id).eq("stock", stock).update();
        //CAS法
        boolean success = update().setSql("stock = stock -1").eq("voucher_id", id).ge("stock", 1).update();
        if(success){
            //5、下单
            VoucherOrder voucherOrder = new VoucherOrder().setId(idWorker.nextId("seckill:voucher")).setVoucherId(id).setUserId(1L);
            voucherOrderMapper.insert(voucherOrder);
            return "seckill sucess!";
        }
        return "No inventory!";
    }

    /* 悲观锁解决多表多条件判断和操作的原子性
     * 场景如下：在秒杀的时候通常需要限制薅羊毛的行为，要求一人一单，此时上述场景中多了一个判断条件，判断此人是否已领，此时需要确保以下三个操作的原子性：
     *   判断此用户是否已领取
     *   判断库存是否满足
     *   扣减库存
     *此时，因为要操作两张表，上述的乐观锁不能满足(上面的乐观锁背后就是利用比较和操作[CAS]原子性的特性实现两步操作原子性执行的特性)，此时需要引入悲观锁，用较大的锁粒度将期望原子性操作的步奏保护起来
     *  总结：多个可变的条件和操作不能使用CAS模式实现原子性操作
     * 所遇到的问题如下：
     * A：按照一人一单的思路，只需要对人的维度加锁，如果此悲观锁加到方法上，那么就会锁住所有用户的请求，我们的目的是单个用户重复购买
     * B： 只对用户加锁后，对库存加锁的逻辑是不是就失效了？
     *     如果不使用CAS方法更新库存，就会出现对库存加锁失败的情况(不同的用户同时进来判断库存)，这里使用CAS方式就会杜绝这种情况，这里判断库存的逻辑还是有必要，因为可以直接结束没有库存的情况
     * C：使用对象锁锁住[判断用户重复购买]、[库存足够]、[更新库存]的逻辑锁住,那么就使用用户id作为对象锁来锁定,使用对象的toString方法得到的字符串是一个新对象，每次toString都会重新创建一个对象，锁不住
     * D：事务失效的场景：事务自调用失效
     *    解决方案：获取注入到spring容器中的代理对象，然后调用
     * E：锁释放和事务提交的先后顺序（同一个事务中未提交的数据可见，但是不同事务就是mysql默认的隔离界别了）
     */
    @Override
    public String seckill3(Long id,Long userId) {
        System.out.println("当前的值为："+userId+"哈希值为:"+userId.hashCode());
        //1、查询秒杀优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherMapper.selectById(id);
        if (seckillVoucher == null) {
            return "param error";
        }
        //2、判断优惠券活动时间
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();
        if (LocalDateTime.now().isBefore(beginTime) || LocalDateTime.now().isAfter(endTime)) {
            return "seckill expire!";
        }
        //A：获取代理对象，然后调用事务方法,并且用锁将事务提交包着
        //注意这里如果使用userId或者userId.toString()直接作为对象锁，锁不住
//        synchronized (userId.toString()) {
//        synchronized (userId) {
        synchronized (userId.toString().intern()) {
            SeckillVoucherService poxy = (SeckillVoucherService)AopContext.currentProxy();
            return poxy.purchaseVoucher(id, seckillVoucher, userId);
        }

    }
    /* 悲观锁锁粒度的推敲
     */
    @Override
    @Transactional
    public String purchaseVoucher(Long id, SeckillVoucher seckillVoucher,Long userId) {

        System.out.println(id.hashCode());
        //3、判断优惠券库存信息(可变的判断条件1)
        Integer stock = seckillVoucher.getStock();
        if (stock <= 0) {
            return "No inventory!";
        }
        //4、判断用户是否已购此优惠券(可变的判断条件2)
        QueryWrapper<VoucherOrder> wrapper = new QueryWrapper<>();
        wrapper.eq("user_id", userId);
        wrapper.eq("voucher_id", id);
        VoucherOrder order = voucherOrderMapper.selectOne(wrapper.orderByAsc("id").last("limit 1"));
        if (order != null) {
            return "Repeat purchase!";
        }
        //5、扣减库存
        //版本号法
        //boolean success = update().setSql("stock = stock -1").eq("voucher_id", id).eq("stock", stock).update();
        //CAS法
        boolean success = update().setSql("stock = stock -1").eq("voucher_id", id).ge("stock", 1).update();
        if (success) {
            //6、下单
            VoucherOrder voucherOrder = new VoucherOrder().setId(idWorker.nextId("seckill:voucher")).setVoucherId(id).setUserId(userId);
            voucherOrderMapper.insert(voucherOrder);
            return "seckill sucess!";
        }
        return "seckill fail!";
    }

    /* 使用redis分布式锁，锁超时自动释放的解决方案[value添加线程标识，防止释放别的线程的锁]
     * 场景如下：实现一人一单，还是对人加锁，内部减库存使用乐观锁
     * 存在的问题：释放锁逻辑中判断和释放两个动作非原子性操作
     *
     */
    @Override
    public String seckill4(Long id, Long userId) {
        System.out.println("当前的值为："+userId+"哈希值为:"+userId.hashCode());
        //1、查询秒杀优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherMapper.selectById(id);
        if (seckillVoucher == null) {
            return "param error";
        }
        //2、判断优惠券活动时间
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();
        if (LocalDateTime.now().isBefore(beginTime) || LocalDateTime.now().isAfter(endTime)) {
            return "seckill expire!";
        }
        //A：使用redis分布式锁，获取分布式互斥锁
        Boolean success = cacheService.tryLock1(userId.toString(), 100L, TimeUnit.SECONDS);
        if(success){
            SeckillVoucherService poxy = (SeckillVoucherService)AopContext.currentProxy();
            String info = poxy.purchaseVoucher(id, seckillVoucher, userId);
//            Boolean delSuccess = cacheService.delLock1(userId.toString());
            //使用lua脚本保证判断和删除的原子性
            Long delSuccess = cacheService.delLock2(userId.toString());
            System.out.println("release redis lock status:"+delSuccess);
            return info;
        }
        return "seckill fail!";
    }

    /* 使用redisson客户端实现分布式锁：可以解决解决获取锁不可重试、锁不可重复、锁超时自动释放问题
     *
     *
     *
     */
    @Override
    public String seckill5(Long id, Long userId) {
        //1、查询秒杀优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherMapper.selectById(id);
        if (seckillVoucher == null) {
            return "param error";
        }
        //2、判断优惠券活动时间
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();
        if (LocalDateTime.now().isBefore(beginTime) || LocalDateTime.now().isAfter(endTime)) {
            return "seckill expire!";
        }
        //A：使用redisson分布式锁，将会用作为锁的key
        RLock lock = redissonClient.getLock(userId.toString());
        try {
            boolean  success = lock.tryLock(2, TimeUnit.SECONDS);
            if(success){
                SeckillVoucherService poxy = (SeckillVoucherService)AopContext.currentProxy();
                //获取代理类对象执行，保证事务不会失效
                String info = poxy.purchaseVoucher(id, seckillVoucher, userId);
                return info;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return "seckill fail!";
    }

    @Override
    public void loadStock(Long id) {
        SeckillVoucher seckillVoucher = seckillVoucherMapper.selectById(id);
        Integer stock = seckillVoucher.getStock();
        cacheService.set("seckill:stock:"+id,stock.toString());
    }

    @Override
    public String seckill6(Long id, Long userId) {
        //1、使用lua脚本判断下单资格和扣减库存
        Long result = stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.emptyList(), id.toString(), userId.toString());
        //2、判断lua脚本返回结果
        if (result!=0) {
            //2.1 如果下单失败，返回失败信息
            return result==1?"库存不足！":"不能重复购买";
        }
        //2.2 如果下单成功，组装消息投入到消息队列,并且返回订单id
        Long orderId = idWorker.nextId("seckill:voucher");
        /*2.3 将订单id、优惠券id、用户id放入消息队列
        * 临时方案，使用阻塞队列实现异步下单
        */
        VoucherOrder voucherOrder = new VoucherOrder().setId(orderId).setVoucherId(id).setUserId(userId);
        //在异步线程中获取不到代理对象，所以提起获取代理对象并作为成员变量
        poxy = (SeckillVoucherService)AopContext.currentProxy();
        blockingQueue.add(voucherOrder);
        return orderId.toString();
    }
    /* 使用阻塞队列代替消息队列，实现异步秒杀
     * 将订单信息放入阻塞队列
     * 创建单线程消费订单消息
     * 在类初始化时调用阻塞队列的消费者进行阻塞消费
     */
    private ArrayBlockingQueue<VoucherOrder> blockingQueue = new ArrayBlockingQueue(1024*1024);
    private ExecutorService SECKILL_ORDER_HANDEL = Executors.newSingleThreadExecutor();

    @PostConstruct
    public void init(){
        SECKILL_ORDER_HANDEL.submit(new vourcherOrderHandler());
    }
    private SeckillVoucherService poxy = null;
    private class vourcherOrderHandler implements Runnable{

        @Override
        public void run() {
            while(true){
                try {
                    //1.从阻塞队列中获取订单
                    VoucherOrder order = blockingQueue.take();
                    //创建订单
                    //2.创建订单
                    poxy.purchaseVoucherSyn(order);

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    /* 使用阻塞队列代替消息队列，实现异步秒杀
     * 将订单信息放入阻塞队列
     * 创建单线程消费订单消息
     */
    @Override
    @Transactional
    public void purchaseVoucherSyn(VoucherOrder order) {
        //4、判断用户是否已购此优惠券(可变的判断条件2)
        QueryWrapper<VoucherOrder> wrapper = new QueryWrapper<>();
        wrapper.eq("user_id", order.getUserId());
        wrapper.eq("voucher_id", order.getVoucherId());
        VoucherOrder userOrder = voucherOrderMapper.selectOne(wrapper.orderByAsc("id").last("limit 1"));
        if (userOrder != null) {
            log.error("Repeat purchase!");
            return;
        }
        //5、扣减库存
        //版本号法
        //boolean success = update().setSql("stock = stock -1").eq("voucher_id", id).eq("stock", stock).update();
        //CAS法
        boolean success = update().setSql("stock = stock -1").eq("voucher_id", order.getVoucherId()).ge("stock", 1).update();
        if (success) {
            //6、下单
            voucherOrderMapper.insert(order);
            log.error("seckill sucess!");
        }
        log.error("seckill fail!");
    }

}
